package com.wanger.swc.util.sql;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.wanger.swc.exception.BusEx;
import com.wanger.swc.util.validation.MyVerifyUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;

import java.io.StringReader;
import java.sql.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Author: wanger
 * @Date: 2025/1/2 22:46
 * @Description:
 */
@Slf4j
public class SQLUtil {

    /**
     * 通过数据库连接conn，执行sql语句，返回结果集
     *
     * @param conn   数据库连接
     * @param sql    sql语句
     * @param type   结果集的类型
     * @param params sql的参数值
     * @param <T>    结果集的类型
     * @return 结果集
     */
    public static <T> List<T> getDataBySQL(Connection conn, String sql, Class<T> type, Object... params) {
        return BeanUtil.copyToList(getDataBySQL(conn, sql, params), type);
    }

    /**
     * 通过数据库连接conn，执行sql语句，返回结果集
     *
     * @param conn   数据库连接
     * @param sql    sql语句
     * @param params 参数值
     * @return 结果集
     */
    public static List<Map<String, Object>> getDataBySQL(Connection conn, String sql, Object... params) {
        MyVerifyUtil.nullVerify(conn, "数据库连接不能为空！");
        MyVerifyUtil.nullVerify(sql, "要执行的sql语句不能为空！");
        List<Map<String, Object>> resultList = new ArrayList<>();
        String tmpSql = fillParameters(sql, params);//检查参数与占位符关系是否合法，并设置参数值

        try (PreparedStatement preparedStatement = conn.prepareStatement(tmpSql)) {
            // 设置参数
            //for (int i = 0; i < params.length; i++) {
            //    preparedStatement.setObject(i + 1, params[i]);
            //}
            try (ResultSet resultSet = preparedStatement.executeQuery()) {
                resultList = getDataByResultSet(resultSet);
            }
        } catch (SQLException e) {
            log.error("sql执行出错！", e);
        }
        return resultList;
    }

    /**
     * 把结果集转换为数据集
     *
     * @param resultSet 结构集
     * @return 数据集
     * @throws SQLException sql异常
     */
    public static List<Map<String, Object>> getDataByResultSet(ResultSet resultSet) throws SQLException {
        List<Map<String, Object>> list = new ArrayList<>();
        // 获取结果集的元数据
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();
        // 遍历 ResultSet
        while (resultSet.next()) {
            Map<String, Object> rowMap = new LinkedHashMap<>();
            for (int i = 1; i <= columnCount; i++) {
                String columnName = metaData.getColumnLabel(i);
                Object value = resultSet.getObject(i);
                rowMap.put(columnName.toLowerCase(), value);
            }
            if (CollUtil.isNotEmpty(rowMap)) list.add(rowMap);
        }
        return list;
    }

    /**
     * sql参数填充
     * //1.1.检查sql中的?占位符数量与args参数数量是否一致，不一致则抛出非法参数异常
     * //1.2.检查sql中的?占位符，要么都是只有?的占位符，要么都是带序号的?占位符，并且序号连续，并且从1开始，否则抛出非法参数异常
     * //2.1.检查sql中的?占位符是否后面有数字顺序，若没有数字序号，则按顺序替换所有?占位符为带顺序的占位符，起始从1起始
     * //2.2.替换sql中的所有占位符为唯一id，防止前面放置的值被后面的值替换掉
     * //3.遍历args参数
     * //3.1.检查args参数的类型，若为CharSequence类型则把参数加上单引号替换到sql中的对应占位符的位置
     * //3.2.检查args参数的类型，若为Number类型则把参数替换到sql中的对应占位符的位置
     * //3.3.检查args参数的类型，如果参数实现了Iterable接口，
     * //3.3.1.则把参数转换为Iterable类型，遍历Iterable中的元素，并准备一个tmpArgs 列表
     * //3.3.2.检查Iterable中的元素类型，若为CharSequence类型则把元素加上单引号添加到tmpArgs
     * //3.3.3.检查Iterable中的元素类型，若为Number类型则直接追加到tmpArgs
     * //3.3.4.检查Iterable中的元素类型，若参数为其他类型，则都统一toString都当字符串处理，并把元素加上单引号添加到tmpArgs
     * //3.3.5.把tmpArgs使用英文逗号分隔，并替换到对应的?占位符位置
     * //3.4.若参数为其他类型，则都统一toString都当字符串处理，并把参数加上单引号替换到sql中的对应占位符的位置
     * //4.返回填充好参数的sql
     *
     * @param sql  带有?占位符的sql语句
     * @param args 要填充的参数
     * @return 返回填充好参数的sql
     */
    public static String fillParameters(String sql, Object... args) {
        args = nullCheck(sql, args);
        sql = checkAndVerify(sql, args);
        String rSql = sql;
        Map<String, String> km = new HashMap<>();
        for (int i = 0; i < args.length; i++) {//参数预处理，防止前面放置的值被后面替换
            String position = "?" + (i + 1);
            String placeholder = UUID.randomUUID().toString().replace("-", "");
            km.put(position, placeholder);
            rSql = rSql.replace(position, placeholder);
        }
        for (int i = 0; i < args.length; i++) {
            String position = "?" + (i + 1);
            String placeholder = km.get(position);
            Object arg = args[i];
            if (arg == null) {
                rSql = rSql.replace(placeholder, "null");
            } else if (arg instanceof CharSequence) {
                rSql = rSql.replace(placeholder, "'" + arg + "'");
            } else if (arg instanceof Number) {
                rSql = rSql.replace(placeholder, "" + arg);
            } else if (arg instanceof Iterable) {
                List<String> parList = new ArrayList<>();
                for (Object iArg : (Iterable<?>) arg) {
                    processingList(iArg, parList);
                }
                rSql = rSql.replace(placeholder, String.join(",", parList));
            } else if (arg.getClass().isArray()) {
                List<String> parList = new ArrayList<>();
                for (Object iArg : (Object[]) arg) {
                    processingList(iArg, parList);
                }
                rSql = rSql.replace(placeholder, String.join(",", parList));
            } else rSql = rSql.replace(placeholder, "'" + arg + "'");
        }
        return rSql;
    }

    /**
     * 处理列表或者数组的元素
     *
     * @param iArg    元素
     * @param parList 提取到的值
     */
    private static void processingList(Object iArg, List<String> parList) {
        if (iArg == null) {
            parList.add("null");
        } else if (iArg instanceof CharSequence) {
            parList.add("'" + iArg + "'");
        } else if (iArg instanceof Number) {
            parList.add(String.valueOf(iArg));
        } else parList.add(String.valueOf(iArg));
    }

    /**
     * 前置检查与校验，若都为数字序号的，则删除占位符与序号之间的空格后返回，若都不带序号，则添加序号后返回
     *
     * @param sql  sql
     * @param args 参数
     * @return 校验后的sql
     */
    private static String checkAndVerify(String sql, Object[] args) {
        int n = 0;
        int isDigital = 0;
        char[] arr = sql.toCharArray();
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == '?') {
                n++;
                while (i + 1 < arr.length) {
                    if (arr[i + 1] == ' ' || arr[i + 1] == '\t' || arr[i + 1] == '\n') i++;
                    else {
                        if (Character.isDigit(arr[i + 1])) {
                            if (isDigital == 0) isDigital = 1;
                            else if (isDigital == -1) BusEx.throwEx("占位符必须都带序号或者都不带序号！");//格式不统一
                        } else {
                            if (isDigital == 0) isDigital = -1;
                            else if (isDigital == 1) BusEx.throwEx("占位符必须都带序号或者都不带序号！");//格式不统一
                        }
                        break;
                    }
                }
                if (i + 1 == arr.length && isDigital == 1) BusEx.throwEx("占位符必须都带序号或者都不带序号！");//格式不统一
            }
        }
        if (isDigital == 1) {
            List<Integer> arrList = extractPlaceholders(sql);//提取占位符后面的序号
            int[] tmpArr = new int[arrList.size()];
            for (int i = 0; i < tmpArr.length; i++) tmpArr[i] = arrList.get(i);
            n = continuityCheck(tmpArr);//连续性检查与起始位检查
            if (n != args.length) BusEx.throwEx("占位符个数与参数个数不匹配！");//个数比匹配
            return formatPlaceholdersAndSerialNumbers(sql);//删除占位符与序号之间的空格
        } else {
            if (n != args.length) BusEx.throwEx("占位符个数与参数个数不匹配！");//个数比匹配
            return appendSequenceNumber(sql);//添加序号
        }
    }

    /**
     * 格式化占位符与序列号，
     *
     * @param sql sql
     * @return 占位符格式化后的sql
     */
    private static String formatPlaceholdersAndSerialNumbers(String sql) {
        char[] arr = sql.toCharArray();
        StringBuilder rSql = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            rSql.append(arr[i]);
            if (arr[i] == '?') {
                StringBuilder tmpStr = new StringBuilder();
                while (i + 1 < arr.length) {
                    if (arr[i + 1] == ' ' || arr[i + 1] == '\t' || arr[i + 1] == '\n') {
                        i++;
                        tmpStr.append(arr[i + 1]);
                    } else {
                        if (!Character.isDigit(arr[i + 1])) rSql.append(tmpStr);
                        break;
                    }
                }
            }
        }
        return rSql.toString();
    }

    /**
     * 添加序号
     *
     * @param sql sql
     * @return 返回添加序号后的sql
     */
    private static String appendSequenceNumber(String sql) {
        char[] arr = sql.toCharArray();
        StringBuilder rSql = new StringBuilder();
        int i = 1;
        for (char c : arr) {
            rSql.append(c);
            if (c == '?') rSql.append(i++);
        }
        return rSql.toString();
    }

    /**
     * 空值检查
     *
     * @param sql  sql
     * @param args 参数
     */
    private static Object[] nullCheck(String sql, Object... args) {
        MyVerifyUtil.nullVerify(sql, "sql不能为空！");
        if (args == null) args = new Object[0];
        return args;
    }

    /**
     * 提取占位符后面的序号
     *
     * @param sql sql
     * @return 返回提取到的序号列表
     */
    private static List<Integer> extractPlaceholders(String sql) {
        List<Integer> placeholders = new ArrayList<>();
        // 使用正则表达式匹配字符串中的?后面带有序号的部分
        Pattern pattern = Pattern.compile("\\?\\s*(\\d+)(?:\\s+|,|;|\\b|$)");
        Matcher matcher = pattern.matcher(sql);
        // 找到所有匹配的序号并添加到列表中
        while (matcher.find()) placeholders.add(Integer.parseInt(matcher.group(1)));
        return placeholders;
    }

    /**
     * 检查序号是否合法
     *
     * @param nums 序号列表
     */
    private static int continuityCheck(int[] nums) {
        // 对数组进行排序
        Arrays.sort(nums);
        // 判断第一个值是否为1
        if (nums[0] != 1) BusEx.throwEx("带序号的占位符必须从1开始！");
        // 遍历剩下的值，判定是否连续递增
        int n = 1;
        for (int i = 1; i < nums.length; i++) {
            // 若连续一样，则快速跳过
            while (i < nums.length && nums[i - 1] == nums[i]) i++;
            // 若不一样，则判定不一样的是否连续递增
            if (i < nums.length) {
                if (nums[i] == n + 1) n++;
                else BusEx.throwEx("带序号的占位符必须连续！");
            }
        }
        return n;
    }

    /**
     * sql查询字段验证
     *
     * @param sql    要验证的sql
     * @param fields 要验证的配置字段
     * @return sql验证通过则返回sql，便于后续处理
     */
    public static String selectFieldVerify(String sql, String... fields) {
        try {
            MyVerifyUtil.nullVerify(sql, "要验证的sql不能为空！");
            MyVerifyUtil.nullVerify(fields, "要验证的字段不能为空！");
            Set<String> fieldSet = Arrays.stream(fields).filter(StrUtil::isNotBlank).collect(Collectors.toSet());//去除空字段与重复字段
            if (fields.length != fieldSet.size()) BusEx.throwEx("配置的验证字段不能为空或者重复！");
            CCJSqlParserManager parserManager = new CCJSqlParserManager();
            Statement parse = parserManager.parse(new StringReader(sql));
            if (parse instanceof Select) {
                Select select = (Select) parse;
                // 获取 SELECT 子句
                PlainSelect plainSelect = select.getPlainSelect();
                //获取到查询字段列表，并转换为小写
                List<String> parseFieldList = plainSelect.getSelectItems()
                        .stream()
                        .map(it -> (it.getAlias() != null ?
                                it.getAlias().getName() :
                                it.getASTNode().jjtGetLastToken().toString()).toLowerCase())
                        .collect(Collectors.toList());
                MyVerifyUtil.nullVerify(parseFieldList, "sql中必须要有select字段！");
                Map<String, Long> fieldMap = parseFieldList.stream().collect(Collectors.groupingBy(e -> e, Collectors.counting()));
                Set<String> duplicateField = fieldMap.entrySet()
                        .stream()
                        .filter(e -> e.getValue() > 1)
                        .map(Map.Entry::getKey)
                        .collect(Collectors.toSet());
                if (CollUtil.isNotEmpty(duplicateField)) {
                    throw new BusEx("sql的select字段【" + StrUtil.join(",", duplicateField) + "】不能重复！");
                }
                //用配置的字段来sql中匹配字段
                Set<String> missingFields = fieldSet.stream().filter(f -> !fieldMap.containsKey(f)).collect(Collectors.toSet());
                if (CollUtil.isNotEmpty(missingFields)) {
                    BusEx.throwEx("sql中必须包含【" + StrUtil.join(",", missingFields) + "】字段");
                }
            } else BusEx.throwEx("只能验证普通查询语句！");
        } catch (JSQLParserException e) {
            BusEx.throwEx("sql解析出错！");
        }
        return sql;
    }
}
